/*
 * Copyright (c) 2011 Norbert Kopcsek
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package org.modulr.bundle {

	import flash.events.ErrorEvent;
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.net.URLLoader;
	import flash.net.URLLoaderDataFormat;
	import flash.net.URLRequest;
	import flash.utils.Dictionary;
	import flash.utils.getDefinitionByName;

	import mx.collections.ArrayList;
	import mx.collections.IList;

	import org.as3commons.logging.ILogger;
	import org.as3commons.logging.LoggerFactory;
	import org.modulr.framework.Framework;

	[Event( name="complete", type="flash.events.Event" )]
	[Event( name="installed", type="org.modulr.bundle.BundleEvent" )]
	[Event( name="loading", type="org.modulr.bundle.BundleEvent" )]
	[Event( name="starting", type="org.modulr.bundle.BundleEvent" )]
	[Event( name="stopping", type="org.modulr.bundle.BundleEvent" )]
	[Event( name="active", type="org.modulr.bundle.BundleEvent" )]
	/**
	 * Manages all bundles loaded by the framework.
	 *
	 * @see org.modulr.bundle.Bundle
	 * @see org.modulr.framework.Framework
	 *
	 * @author Norbert Kopcsek
	 */
	public class BundleRegistry extends EventDispatcher {


		//------------------------------------------------------------------------------
		//
		//   Statics 
		//
		//------------------------------------------------------------------------------

		private static var logger:ILogger = LoggerFactory.getLogger( "BundleRegistry" );


		//------------------------------------------------------------------------------
		//
		//   Constructor 
		//
		//------------------------------------------------------------------------------

		public function BundleRegistry( framework:Framework ) {
			this.framework = framework;

			_bundles = new ArrayList();
			bundlesById = new Dictionary();
		}


		//------------------------------------------------------------------------------
		//
		//   Properties (Getter/Setter) 
		//
		//------------------------------------------------------------------------------

		//--------------------------------------
		// bundles 
		//--------------------------------------

		/**
		 * The list of bundles installed in this registry.
		 */
		public function get bundles():IList {
			return _bundles;
		}


		//------------------------------------------------------------------------------
		//
		//   Protected fields 
		//
		//------------------------------------------------------------------------------

		/**
		 * @private
		 */
		protected var _bundles:IList;


		/**
		 * @private
		 */
		protected var bundlesById:Dictionary;

		/**
		 * @private
		 */
		protected var framework:Framework;


		//------------------------------------------------------------------------------
		//
		//  Methods 
		//
		//------------------------------------------------------------------------------

		/**
		 * Loads a bundle xml using the given url request.
		 * @param urlRequest An URLRequest pointing to the bundles xml file.
		 */
		public function loadBundlesXml( urlRequest:URLRequest ):void {
			var bundlesXmlLoader:URLLoader = new URLLoader();
			bundlesXmlLoader.dataFormat = URLLoaderDataFormat.TEXT;
			bundlesXmlLoader.addEventListener( Event.COMPLETE, bundlesXmlLoader_completeHandler ); // no weak reference
			bundlesXmlLoader.load( urlRequest );
		}

		/**
		 * Loads all mandatory bundles.
		 */
		public function loadMandatoryBundles():void {

			logger.debug( "Load mandatory bundles" );

			var bundle:Bundle;

			for ( var i:int = 0; i < bundles.length; i++ ) {
				bundle = bundles.getItemAt( i ) as Bundle;

				if ( bundle.state == BundleState.INSTALLED && bundle.mandatory ) {
					loadBundle( bundle );
				}
			}
		}

		/**
		 * Loads a bundle for a given bundle descriptor in a bundle context unless it is already loaded.
		 * @param bundle The bundle to be loaded.
		 */
		public function loadBundle( bundle:Bundle ):void {
			if ( bundle == null ) {
				return;
			}
			tryLoadBundle( bundle );
		}



		/**
		 * Unloads the given bundle.
		 * @param bundle The bundle to be unloaded.
		 */
		public function unloadBundle( bundle:Bundle ):void {
			logger.info( "Unload bundle {0} ({1})", bundle.bundleId, bundle.version );
			deactivateBundle( bundle );
			bundle.moduleInfo.unload();
		}

		/**
		 * Returns the bundle for the given id or null if no bundle can be found.
		 * @param id The id of the bundle
		 * @return The bundle with the given id or null.
		 */
		public function getBundleById( id:String ):Bundle {
			if ( containsBundle( id )) {
				return bundlesById[ id ];
			} else {
				throw new BundleNotFoundError( id );
			}
		}

		/**
		 * Checks whether a bundle with the given id is installed or not.
		 * @param id The id of the bundle
		 * @return A boolean indicating whether a bundle with the given id is installed.
		 */
		public function containsBundle( id:String ):Boolean {
			return bundlesById.hasOwnProperty( id );
		}

		/**
		 * Parses bundle.xml and creates bundle descriptors.
		 * @param xml The xml object to be parsed.
		 */
		public function installBundlesFromXml( xml:XML ):void {
			for each ( var bundleXml:XML in xml.bundle ) {
				if ( !containsBundle( bundleXml.@id.toString())) {
					installBundle( createBundleFromXml( bundleXml ));
				}
			}
		}

		/**
		 * Installs a bundle at the bundle registry.
		 * @param bundle The bundle to be installed.
		 * @return The bundle registered at the bundle registry. This is not necessarily the bundle given as param.
		 */
		public function installBundle( bundle:Bundle ):Bundle {

			if ( containsBundle( bundle.bundleId )) {
				return getBundleById( bundle.bundleId );
			}

			bundle.context = new BundleContext( framework, bundle );

			bundlesById[ bundle.bundleId ] = bundle;
			bundles.addItem( bundle );

			dispatchEvent( new BundleEvent( BundleEvent.INSTALLED, bundle ));

			return bundle;
		}

		/**
		 * Creates a bundle from xml data.
		 * @param bundleXml The xml data for a single bundle.
		 * @return The bundle created from the xml data.
		 */
		public function createBundleFromXml( bundleXml:XML ):Bundle {
			if ( containsBundle( bundleXml.@id.toString())) {
				return getBundleById( bundleXml.@id.toString());
			}

			var bundle:Bundle = new Bundle( bundleXml.@id.toString(), bundleXml.@file.toString(), bundleXml.@activator.toString(), bundleXml.@mandatory == "true", bundleXml.@rsl == "true", bundleXml.@version.toString());

			var requiredBundleXmlList:XMLList = bundleXml.requiredBundle;

			for ( var i:int = 0; i < requiredBundleXmlList.length(); i++ ) {
				var requiredBundleXml:XML = requiredBundleXmlList[ i ];
				var requiredBundleId:String = requiredBundleXml.@id.toString();
				var lazy:Boolean = requiredBundleXml.@lazy != null ? requiredBundleXml.@lazy.toString() == "true" : false;
				var optional:Boolean = requiredBundleXml.@optional != null ? requiredBundleXml.@optional.toString() == "true" : false;

				var requiredBundle:RequiredBundle = new RequiredBundle( requiredBundleId, optional, lazy );
				bundle.requiredBundles.push( requiredBundle );
			}
			return bundle;
		}

		/**
		 * Adds event listeners for the specified IBundleListener.
		 * @param listener The bundle listener to be added.
		 */
		public function addBundleListener( listener:IBundleListener ):void {
			addEventListener( BundleEvent.LOADING, listener.bundleChanged ); // no weak reference
			addEventListener( BundleEvent.STARTING, listener.bundleChanged ); // no weak reference
			addEventListener( BundleEvent.STOPPING, listener.bundleChanged ); // no weak reference
			addEventListener( BundleEvent.ACTIVE, listener.bundleChanged ); // no weak reference
		}

		/**
		 * Removes event listeners of the specified IBundleListener.
		 * @param listener The bundle listener to be removed.
		 */
		public function removeBundleListener( listener:IBundleListener ):void {
			removeEventListener( BundleEvent.LOADING, listener.bundleChanged );
			removeEventListener( BundleEvent.STARTING, listener.bundleChanged );
			removeEventListener( BundleEvent.STOPPING, listener.bundleChanged );
			removeEventListener( BundleEvent.ACTIVE, listener.bundleChanged );
		}


		//------------------------------------------------------------------------------
		//
		//   Protected methods 
		//
		//------------------------------------------------------------------------------

		/**
		 * Loads the bundle if it's not yet loading.
		 * @private
		 * @param bundle The bundle which should be loaded if it's not yet loading.
		 */
		protected function tryLoadBundle( bundle:Bundle ):void {

			if ( bundle.state != BundleState.INSTALLED ) {
				return;
			}

			logger.info( "Loading bundle {0} ({1})", bundle.bundleId, bundle.version );

			installDependencies( bundle );
			loadDependencies( bundle );

			var bundleLoader:BundleLoader = new BundleLoader( bundle, framework.domain );
			addEventListenersToBundleLoader( bundleLoader );
			bundleLoader.load();

			bundle.state = BundleState.LOADING;
			dispatchEvent( new BundleEvent( BundleEvent.LOADING, bundle ));
		}

		/**
		 * Installs the dependencies of the given bundle.
		 * @private
		 * @param bundle The bundle for which the dependencies should be installed.
		 */
		protected function installDependencies( bundle:Bundle ):void {
			for ( var i:uint = 0; i < bundle.requiredBundles.length; i++ ) {
				var requiredBundle:RequiredBundle = bundle.requiredBundles[ i ] as RequiredBundle;

				if ( requiredBundle.bundle == null ) {
					requiredBundle.bundle = bundlesById[ requiredBundle.bundleId ];
					requiredBundle.bundle.requiredByBundles.push( bundle );
				}
			}
		}

		/**
		*  Loads the dependencies of the given bundle.
		 * @private
		 * @param bundle The bundle of which the dependencies should be loaded.
		*/
		protected function loadDependencies( bundle:Bundle ):void {
			for ( var i:uint = 0; i < bundle.requiredBundles.length; i++ ) {
				var requiredBundle:RequiredBundle = bundle.requiredBundles[ i ] as RequiredBundle;
				var actualRequiredBundle:Bundle = bundlesById[ requiredBundle.bundleId ] as Bundle;
				tryLoadBundle( actualRequiredBundle );
			}
		}

		/**
		 * Queues a bundle for deferred loading.
		 * @private
		 * @param bundle The bundle to be queued.
		 */
		protected function queueBundle( bundle:Bundle ):void {
			logger.info( "Queuing bundle {0} ({1})", bundle.bundleId, bundle.version );
			bundle.state = BundleState.QUEUED;
		}

		/**
		 * Tries to activate queued bundles of the given list of bundles.
		 * @private
		 * @param bundles The bundles which should be checked for activation.
		 */
		protected function tryActivateQueuedBundles( bundles:Array ):void {
			for ( var i:int = 0; i < bundles.length; i++ ) {
				var bundle:Bundle = bundles[ i ] as Bundle;

				if ( bundle.state == BundleState.QUEUED && areRequiredBundlesActive( bundle )) {
					activateBundle( bundle );
				}
			}
		}

		/**
		 * Checks whether all required bundles of the given bundle are active or not.
		 * @private
		 * @param bundle The bundle to be checked.
		 * @return Returns only true if all required bundles are active.
		 */
		protected function areRequiredBundlesActive( bundle:Bundle ):Boolean {
			for ( var i:uint = 0; i < bundle.requiredBundles.length; i++ ) {
				var requiredBundle:RequiredBundle = bundle.requiredBundles[ i ] as RequiredBundle;

				if (( bundlesById[ requiredBundle.bundleId ] as Bundle ).state != BundleState.ACTIVE ) {
					return false;
				}
			}
			return true;
		}


		/**
		 * Activates the given bundle.
		 * @private
		 * @param bundle The bundle to be activated.
		 */
		protected function activateBundle( bundle:Bundle ):void {

			logger.info( "Activate bundle {0} ({1})", bundle.bundleId, bundle.version );

			if ( !bundle.isRsl || bundle.activatorName != null ) {
				var activator:IBundleActivator = getActivatorOfBundle( bundle );

				if ( activator !== null ) {
					bundle.activator = activator;

					bundle.state = BundleState.STARTING;
					dispatchEvent( new BundleEvent( BundleEvent.STARTING, bundle ));

					activator.start( bundle.context );
				}
			}
			bundle.state = BundleState.ACTIVE;
			dispatchEvent( new BundleEvent( BundleEvent.ACTIVE, bundle ));

			tryActivateQueuedBundles( bundle.requiredByBundles );
		}

		/**
		 * Deactivates the given bundle.
		 * @private
		 * @param bundle The bundle to be deactivated.
		 */
		protected function deactivateBundle( bundle:Bundle ):void {

			logger.info( "Deactivate bundle {0} ({1})", bundle.bundleId, bundle.version );

			bundle.state = BundleState.STOPPING;
			dispatchEvent( new BundleEvent( BundleEvent.STOPPING, bundle ));

			var index:int = bundles.getItemIndex( bundle );

			if ( index >= 0 ) {
				bundles.removeItemAt( index );
			}

			var bundleActivator:IBundleActivator = bundle.activator;

			if ( bundleActivator != null ) {
				bundleActivator.stop( bundle.context );
			}
		}



		/**
		 * Identifies and returns the bundle activator.
		 * 1) Checks if activator is defined in bundle descriptor.
		 * 2) Checks if module is an activator.
		 * 3) Checks if module has activator property.
		 * @private
		 * @param bundle The bundle of which the activator should be returned
		 * @returns The bundle activator of the given bundle.
		 */
		protected function getActivatorOfBundle( bundle:Bundle ):IBundleActivator {
			var activator:IBundleActivator;

			if ( bundle.activatorName !== null && bundle.activatorName !== '' ) {
				var definition:Class = getDefinitionByName( bundle.activatorName ) as Class; //ApplicationDomain.currentDomain.getDefinition( bundle.activatorName ) as Class;

				if ( definition !== null ) {
					var product:Object = new definition(); //NO PMD UseObjectType
					activator = product as IBundleActivator;
				}
			}

			// TODO We need more info about the module, whether it was loading the moduleLoader or not
			if ( activator === null && bundle.moduleInfo !== null && bundle.moduleInfo.factory !== null ) {
				var module:Object = bundle.moduleInfo.factory.create(); //NO PMD UseObjectType

				if ( module is IBundleActivator ) {
					activator = module as IBundleActivator;
				}
			}

			if ( activator === null && module !== null && module.hasOwnProperty( "activator" ) && module.activator !== null ) {
				activator = module.activator as IBundleActivator;
			}
			return activator;
		}

		/**
		 * Adds event listeners to the given bundle loader.
		 * @private
		 * @param bundleLoader The bundle loader to which the event listeners are added to.
		 */
		protected function addEventListenersToBundleLoader( bundleLoader:BundleLoader ):void {
			bundleLoader.addEventListener( Event.COMPLETE, bundleLoader_completeHandler ); // no weak reference
			bundleLoader.addEventListener( ErrorEvent.ERROR, bundleLoader_errorHandler ); // no weak reference
		}

		/**
		 * Removes event listeners from the given bundle loader.
		 * @private
		 * @param bundleLoader The bundle loader to which the event listeners are removed from.
		 */
		protected function removeEventListenersFromBundleLoader( bundleLoader:BundleLoader ):void {
			bundleLoader.removeEventListener( Event.COMPLETE, bundleLoader_completeHandler );
			bundleLoader.removeEventListener( ErrorEvent.ERROR, bundleLoader_errorHandler );
		}

		/**
		 * Called when bundle.xml is loaded.
		 * @private
		 * @param event
		 */
		protected function bundlesXmlLoader_completeHandler( event:Event ):void {
			var bundlesXmlLoader:URLLoader = event.target as URLLoader;
			bundlesXmlLoader.removeEventListener( Event.COMPLETE, bundlesXmlLoader_completeHandler );
			// TODO Need to check if this is proper xml
			// TODO Test this stuff
			installBundlesFromXml( new XML( event.target.data ));

			dispatchEvent( new Event( Event.COMPLETE ));
			loadMandatoryBundles();
		}

		/**
		 * Called when the bundle loader completed loading a bundle.
		 * @private
		 * @param event
		 */
		protected function bundleLoader_completeHandler( event:Event ):void {
			var bundleLoader:BundleLoader = event.target as BundleLoader;
			removeEventListenersFromBundleLoader( bundleLoader );

			var bundle:Bundle = bundleLoader.bundle;
			logger.info( "Loaded bundle {0}", bundle.bundleId );

			if ( areRequiredBundlesActive( bundle )) {
				activateBundle( bundle );
			} else {
				queueBundle( bundle );
			}
		}

		/**
		 * Called when the bundle loader failed loading a bundle.
		 * @private
		 * @param event
		 */
		protected function bundleLoader_errorHandler( event:ErrorEvent ):void {
			var bundleLoader:BundleLoader = event.target as BundleLoader;
			removeEventListenersFromBundleLoader( bundleLoader );

			var bundle:Bundle = bundleLoader.bundle;
			logger.error( "Could not load bundle {0} from {1}", bundle.bundleId, bundle.url );
			bundle.state = BundleState.ERROR;
			dispatchEvent( new BundleEvent( BundleEvent.ERROR, bundle ));
		}
	}
}

